/* ****************************************************

Программа состоит из токенов, разделённых пробелами.
Комментарии начинаются с # и заканчиваются в конце строки.

Токены бывают двух видов (this.is_cmd):
- числа
  - для них создаётся специальный объект-константа
- команды
  - если это команда, то она выполняется
  - если токен не совпадает ни с одной командой,
    то для него создаётся команда взятия значения
    по имени переменной.

**************************************************** */

function parse(x) {
  // возвращает список объектов-токенов вида:
  // [ { value: 'asdf', is_command: true, line: 1, row: 1 },
  //   { value: -12.5, is_command: false, line: 1, row: 6 } ]
  var matcher = function(start, inv, cont, is_cmd, new_line) {
    this.start = start;
    this.inv = inv;
    this.cont = cont;
    this.is_cmd = is_cmd;
    this.new_line = new_line;
  };
  var char_class = function(c) {
    this.chars = c;
  };
  char_class.prototype.isinclass = function(c) {
    return (this.chars.indexOf(c) >= 0);
  };
  var const_letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
  var const_digits = '0123456789';
  var class_varname = new char_class(const_letters + '_');
  var class_varname_tail = new char_class(const_letters + '_' + const_digits);
  var class_number = new char_class(const_digits + '.-');
  var class_comment_start = new char_class('#');
  var class_new_line = new char_class('\n');
  var class_space = new char_class(' \t');
  var re = [
    new matcher(class_varname, false, class_varname_tail, true, false),
    new matcher(class_number, false, class_number, false, false),
    new matcher(class_comment_start, true, class_new_line, null, false),
    new matcher(class_space, false, class_space, null, false),
    new matcher(class_new_line, false, new char_class(''), null, true)
  ];
  this.list_of_toks = [];
  var line = 1;
  var row = 1;
  while (x.length > 0) {
    var c = x.substr(0, 1);
    var invalid_char = true;
    for (var i = 0; i < re.length; ++i) {
      var pt = re[i];
      //L({'try': [i, re[i], c]});
      if (pt.start.isinclass(c)) {
        invalid_char = false;
        var a = c;
        x = x.substr(1);
        while (x.length > 0) {
          c = x.substr(0, 1);
          var p = pt.cont.isinclass(c);
          if (pt.inv) {
            p = !p;
          }
          if (p) {
            a += c;
            x = x.substr(1);
          } else {
            break;
          }
        }
        if (pt.is_cmd != null) {
          var v = a;
          if (! pt.is_cmd) {
            v = parseFloat(a);
            if (isNaN(parseFloat(v)) || !isFinite(v)) {
              throw ({ // TODO: message...
                'error': 'FLOATING POINT ERROR',
                'value': a, 'pos': [line, row]
              });
            }
          }
          this.list_of_toks.push({
            'value': v,
            'is_command': pt.is_cmd,
            'line': line,
            'row': row,
            'repr': v + '@' + line + ':' + row
          });
        }
        if (pt.new_line) {
          line += 1;
          row = 1;
        } else {
          row += a.length;
        }
        break;
      }
    }
    if (invalid_char) {
      throw {
        message: 'Invalid character ' + c + ' at ' + line + ':' + row,
        stack: [] // NOT USED?
      };
    }
  }
  this.index = 0;
}

parse.prototype.next = function() {
  if (this.index >= this.list_of_toks.length) {
    return null;
  }
  //L('get next: ' + this.index + ' :: ' + this.list_of_toks[this.index]);
  return this.list_of_toks[this.index++];
};

/* ****************************************************

Исполнитель команд в контексте.

Результаты исполнения передаются через контекст, переменную "_".

**************************************************** */

function command_executor(cmd, ctx) {
  this.cmd_gen = new cmd_gen(cmd);
  this.ctx = ctx;
  this.current_executor = null;
}

command_executor.prototype.step = function() {
  if (this.current_executor == null) {
    // результат работы предыдущей команды передаётся через
    // контекст; на его основании, анализатор может принять
    // решение, какую следующую команду выдать
    var c = this.cmd_gen.next(this.ctx);
    if (c == null) {
      return true; // done
    }
    this.current_executor = new command_executor(c, this.ctx);
  } else {
    var r;
    try {
      r = this.current_executor.step();
    } catch (err) {
      //L(err);
      //L(this.current_executor.cmd_gen.cmd.first_token);
      err.stack.push(this.current_executor.cmd_gen.cmd.first_token);
      throw err;
    }
    if (r) {
      // процесс выполнения завершён; удаляем исполнителя
      this.current_executor = null;
    }
  }
  // сюда надо ещё вернуться за следующим исполнителем
  return false;
};

/* ****************************************************

Классы с командами.

Каждый класс умеет формировать из потока токенов объект
соответствующей команды.

**************************************************** */

function cmd(first_token, toks, config) {
  //L('CMD:' + first_token.value);
  this.first_token = first_token;
  this.args = []; // сюда попадают команды, подлежащие исполнению
  this.names = []; // сюда попадают имена переменных
  this.result = config.result;
  this.assign = config.assign;
  this.effect = config.effect;
  this.effect_set = config.effect_set;
  this.ctx_effect = config.ctx_effect;
  // это атомарная операция; её результат можно получить
  // в один запрос; об этом сигнализирует наличие метода value
  this.next = config.next;
  //L(this.assign);
  var i;
  var v;
  if (config.args) {
    for (i = 0; i < config.args.length; ++i) {
      if (config.args[i]) {
        v = eat_command(toks);
        if (v === null) {
          throw {
            // TODO: too much of copypaste [A:1]
            'message': 'Unexpected end of code' +
                       (this.first_token ?
                       ' when creating ' + (i + 1) +
                       '-th argument for command ' +
                       this.first_token.repr : '') +
                       '. (Waiting for command)'
          };
        }
        this.args.push(v);
      } else {
        v = toks.next();
        if (v === null) {
          throw {
            // TODO: too much of copypaste [A:2]
            'message': 'Unexpected end of code' +
                       (this.first_token ?
                       ' when creating ' + (i + 1) +
                       '-th argument for command ' +
                       this.first_token.repr : '') +
                       '. (Waiting for name)'
          };
        }
        this.names.push(v);
      }
    }
  } else {
    // специальный случай для блоков
    var cm;
    while ((cm = eat_command(toks)) != null) {
      this.args.push(cm);
    }
  }
  //L(this.args);
}

/* ****************************************************

Классы генератора команд. Создаётся на основе команды
каждый раз снова. Это позволяет выполнять рекурсивные вызовы

В конфигурации команды есть атрибут, управляющие потомком
команд, создаваемым этим генератором.

**************************************************** */

function cmd_gen(c) {
  //L('create generator for ' + (c.first_token ? c.first_token.value : '[null]'));
  this.cmd = c;
  this.step = 0;
  this.values = [];
  if (c.next) {
    this.next = c.next;
  }
}

cmd_gen.prototype.next = function(ctx) {
  //L('step = ' + (c.first_token ? c.first_token.value : '[null]') + ' : ' + this.step);
  if (this.step > 0) {
    //L('push!');
    this.values.push(ctx.variables._);
  }
  if (this.step < this.cmd.args.length) {
    //L('ste:' + this.step);
    return this.cmd.args[this.step++];
  }
  //L('results:');
  //L(this.values);
  // элементарные вычисления без присвоений (кроме _)
  if (this.cmd.result) {
    ctx.variables._ = this.cmd.result(this.values);
    //L('res:' + ctx.variables._);
  }
  // элиментарные присвоения без вычислений
  if (this.cmd.assign) {
    for (var i = 0; i < this.values.length; ++i) {
      ctx.variables[this.cmd.assign[i]] = this.values[i];
    }
    //L(ctx.variables);
  }
  // операции с сайд-эффектом, затрагивающими только фиксированные переменные
  if (this.cmd.effect) {
    ctx.variables._ = this.cmd.effect(ctx.variables, this.values);
  }
  // операции с сайд-эффектом, затрагивающими произвольные переменные
  if (this.cmd.effect_set) {
    ctx.variables._ = this.cmd.effect_set(
      ctx.variables,
      this.values,
      this.cmd.names
    );
  }
  // операции с сайд-эффектом над полным контекстом
  // TODO: надо бы это всё причесать, а то зоопарк
  if (this.cmd.ctx_effect) {
    ctx.variables._ = this.cmd.ctx_effect(ctx, this.values);
  }
  return null;
};

/* ****************************************************

Команды и их конфигурации.

Атрибут args используется для формирования объектов команд
из потока токенов (конструирование).

Остальные атрибуты используются генератором потока команд.
effect_set - [кандидат на удаление, используется только одни раз]
  служит для присвоения вычисленного значения заданной переменной;
  завершающий.
result - вычисляет результат; завершающий
assign - присвоение результатов соответствующим переменным; завершающий
ctx_effect - изменяет контекст; завершающий
next - переопределяет стандартное поведение, должен полностью
  брать на себя генерацию следующих команд. существует важный
  отдельный случай: для атомарных команд не анализируется номер
  шага, а просто выполняется необходимое действие и возвращается null
  (признак того, что новых команд нет).

**************************************************** */

commands_map = {
  'begin': {},
  'set': {
    args: [false, true],
    effect_set: function(vv, v, n) {return vv[n[0].value] = v[0];}
  },
  'minus': {
    args: [true],
    result: function(v) {return - v[0];}
  },
  'incr': {
    args: [false],
    next: function(c) {
      // независимо от шага сразу формируем результат
      c.variables._ = (c.variables[this.cmd.names[0].value] += 1);
      return null;
    }
  },
  'decr': {
    args: [false],
    next: function(c) {
      // независимо от шага сразу формируем результат
      c.variables._ = (c.variables[this.cmd.names[0].value] -= 1);
      return null;
    }
  },
  'sum': {
    args: [true, true],
    result: function(v) {
      return v[0] + v[1];
    }
  },
  'diff': {
    args: [true, true],
    result: function(v) {
      return v[0] - v[1];
    }
  },
  'prod': {
    args: [true, true],
    result: function(v) {
      return v[0] * v[1];
    }
  },
  'quot': {
    args: [true, true],
    result: function(v) {
      return v[0] / v[1];
    }
  },
  'if': {
    args: [true, true],
    next: function(c) {
      if (this.step === 0) {
        this.step = 1;
        return this.cmd.args[0];
      } else if (this.step === 1) {
        this.step = 2;
        if (c.variables._ <= 0) {
          return null;
        }
        return this.cmd.args[1];
      }
      return null;
    }
  },
  'ifelse': {
    args: [true, true, true],
    next: function(c) {
      if (this.step === 0) {
        this.step = 1;
        return this.cmd.args[0];
      } else if (this.step === 1) {
        this.step = 2;
        if (c.variables._ <= 0) {
          return this.cmd.args[2];
        }
        return this.cmd.args[1];
      }
      return null;
    }
  },
  'repeat': {
    args: [true, true],
    next: function(c) {
      if (this.step === 0) {
        this.step = 1;
        return this.cmd.args[0];
      } else if (this.step === 1) {
        this.step = 2;
        this.count = c.variables._; // создаём новое свойство объекта
      }
      if (this.count <= 0) {
        return null;
      }
      --this.count;
      return this.cmd.args[1];
    }
  },
  'local': {
    args: [true],
    next: function(c) {
      if (this.step === 0) {
        this.step = 1;
        c.push();
        return this.cmd.args[0];
      }
      c.pop();
      return null;
    }
  },
  'def': {
    args: [false, true],
    next: function(c) {
      // независимо от шага сразу формируем результат
      c.functions[this.cmd.names[0].value] = this.cmd.args[0];
      return null;
    }
  },
  'call': {
    args: [false],
    next: function(c) {
      if (this.step === 0) {
        this.step = 1;
        var v = this.cmd.names[0].value;
        var f = c.functions[v];
        if (f === undefined) {
          // TODO: проверку наличия переменных надо вынести в контекст,
          // но оттуда будет трудно вынимать исключения... надо подумать
          throw {
            message: 'Unknown function ' + v,
            stack: []
          };
        }
        return f;
      }
      return null;
    }
  },
  'jump': {
    args: [true, true],
    ctx_effect: function(c, v) {
      c.move(v[0], v[1]);
      return 1;
    }
  },
  'draw': {
    args: [true, true],
    ctx_effect: function(c, v) {
      c.move(v[0], v[1]);
      c.gc.line(
        c.variables._xo, c.variables._yo, c.variables._x, c.variables._y,
        c.variables._r, c.variables._g, c.variables._b,
        c.variables._s * c.variables._w
      );
      return 1;
    }
  },
  'clear': {
    args: [],
    next: function(c) {
      // независимо от шага сразу формируем результат
      c.gc.clear(c.variables._r, c.variables._g, c.variables._b);
      return null;
    }
  },
  'left': {
    args: [true],
    effect: function(vv, v) {
      return vv._a += v[0];
    }
  },
  'right': {
    args: [true],
    effect: function(vv, v) {
      return vv._a -= v[0];
    }
  },
  'color': {
    args: [true, true, true],
    assign: ['_r', '_g', '_b']
  },
  'width': {
    args: [true],
    assign: ['_w']
  },
  'scale': {
    args: [true],
    effect: function(vv, v) {
      return vv._s *= v[0];
    }
  },
  'print': {
    args: [true],
    //effect: function(vv, v) { console.log(vv); },
    result: function(v) {
      var x = v[0];
      //L(x);
      throw {
        message: 'Print ' + v[0],
        stack: []
      };
    }
  }
};

function eat_command(toks) {
  var c = toks.next();
  if (! c) {
    // токены кончились; вовремя ли?
    return null;
  }
  var cl;
  if (c.is_command) {
    var v = c.value;
    cl = commands_map[v];
    if (cl === undefined) {
      if (v === 'end') {
        // специальный случай для завершения блока
        return null;
      }
      // видимо, это переменная, создаём конфигурацию для получения значения
      // NOTE: для переменной всегда создаётся конструкция, допускающая
      // чтения значения, хотя, существую случаи, когда читать значение
      // не нужно (присвоением, инкремент, декремент).
      cl = {
        args: [],
        next: function(c) {
          // независимо от шага сразу формируем результат
          try {
            // TODO: чтение переменных происходит только здесь,
            // возможно, проверку существования переменных
            // надо вынести сюда.
            c.variables._ = c.get_val([this.cmd.first_token.value]);
          } catch (err) {
            throw err;
          }
          return null;
        }
      };
    }
  } else {
    // это не команда, просто возвращаем значение
    cl = {
      args: [],
      next: function(c) {
        c.variables._ = this.cmd.first_token.value;
        return null;
      }
    };
  }
  return new cmd(c, toks, cl);
}

// -------------------------------------------
// -------------------------------------------

function context() {
  this.variables = {
    '_xo': 0,
    '_yo': 0,
    '_x': 0,
    '_y': 0,
    '_a': 0,
    '_r': 0,
    '_g': 0,
    '_b': 0,
    '_w': 3,
    '_s': 1,
    '_': 0
  };
  this.functions = {};
  this.gc = null;
  this.stack = [];
}

context.prototype.get_val = function(n) {
  // TODO: надо подумать. строго говоря, операция
  // чтения значения переменной происходит в одном
  // единственном месте. очень вероятно, что проверку
  // надо выполнять именно там
  var x = this.variables[n];
  if (x !== undefined) {
    return x;
  } else {
    throw {
      message: 'Unknown variable ' + n,
      stack: []
    };
  }
};

context.prototype.push = function() {
  //L('>>');
  var v = {};
  for (var k in this.variables) {
    v[k] = this.variables[k];
  }
  this.stack.push(v);
};

context.prototype.pop = function() {
  //L('<<' + this.stack.length);
  var x = this.variables['_'];
  this.variables = this.stack.pop();
  this.variables['_'] = x;
};

context.prototype.move = function(xx, yy) {
  var vv = this.variables;
  vv._xo = vv._x;
  vv._yo = vv._y;
  var a = vv._a * Math.PI / 180.0;
  var c = Math.cos(a);
  var s = Math.sin(a);
  var dx = vv._s * xx;
  var dy = vv._s * yy;
  vv._x += dx * c - dy * s;
  vv._y += dy * c + dx * s;
};

if (typeof exports !== 'undefined') { // NodeJS
  exports.commands_map = commands_map;
  L = function(x) {
    var util = require('util');
    console.log(util.inspect(x, false, 4));
  };
} else { // Browser
  L = function(x) {
    if (typeof console !== 'undefined') {
      console.log(x);
    }
  };
}
